package hit.core;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import hit.reports.Visitor;

/**
 * Indexes all the {@link Product}s in the HIT system. Allows the user to get
 * Products by {@link Barcode}.
 * */
@SuppressWarnings("serial")
public class ProductIndex extends HITBase {
    private Map<Barcode, Product> allProducts;
    private Map<ProductContainer, Set<Product>> productsByContainer;
	
    /**
     * Default Constructor
     */
    public ProductIndex() {
    	
    	allProducts = new HashMap<Barcode, Product>();
    	productsByContainer = new HashMap<ProductContainer, Set<Product>>();
    }
    
    /**
	 * Retrieves a {@link Product} by its {@link Barcode}.
	 * @param barcode A valid {@link Barcode}
	 * @return {@link Product} with matching {@link Barcode}, null if not found.
	 * @throws IllegalArgumentException if Barcode is invalid.
	 */
	public Product GetByBarcode(Barcode barcode) throws IllegalArgumentException{
		
		if (!Barcode.IsValid(barcode))
			throw new IllegalArgumentException();
		
		return allProducts.get(barcode);
	}
	
	/**
	 * The iterator for all the products in the system.
	 * @return The iterator for products.
	 */
	public Iterator<Product> GetProductIterator() {
		
		return allProducts.values().iterator();
	}
	
	/**
	 * Returns the Iterator for a set of {@link Product}s contained 
	 * 	within a particular {@link ProductContainer}
	 * @param container The {@link ProductContainer} to search for
	 * @return The iterator for the given set. Null if not found.
	 */
	public Iterator<Product> GetByProductContainer(ProductContainer container) {
		
		if (!productsByContainer.containsKey(container))
			return null;
		
		return productsByContainer.get(container).iterator();
	}
	
	/**
	 * Returns the ProductContainer within the tree of ProductContainers passed in that 
	 * contains the given Product
	 * @param root the ProductContainer at the root of the tree
	 * @param product the Product to find
	 * @return the ProductContainer that contains the Product or null
	 * @throws IllegalArgumentException thrown if either parameter is null
	 * */
	public ProductContainer FindProductContainer(ProductContainer root, Product product) {
		if (root == null || product == null)
			throw new IllegalArgumentException("Invalid parameters");
		
		Iterator<ProductContainer> containers = product.GetProductContainerIterator();
		
		//loop through the containers, seeing if any matches the root or is the child of root
		while (containers.hasNext()) {
			ProductContainer container = containers.next();
			
			//if it is the same, return the root
			if (container == root) {
				return root;
			}
			
			//otherwise, keep going up until we hit null or get root
			ProductContainer parent = container.GetParentContainer();
			while (parent != null) {
				if (parent == root)
					return container;
				
				parent = parent.GetParentContainer();
			}
		}
		
		return null;
	}
	
	
	/**
	 * Checks to see if the new {@link Product} is valid to be added to the list.
	 * @param product The {@link Product} to check.
	 * @return True if the product can be added, false otherwise.
	 */
	public boolean CanAddProduct(Product product) {	
		
		if (!Product.IsValid(product))
			return false;
	
		if (allProducts.get(product.GetBarcode()) != null)
			return false;
	
		return true;
	}
	
	
	/**
	 * Checks to see if an existing product can be added to an existing container
	 * @param product The product to test.
	 * @param container The container to add it to.
	 * @return
	 */
	public boolean CanAddProductToContainer(Product product, ProductContainer container) {
		
		if (!Product.IsValid(product) || !container.IsValid())
			return false;
		
		// Check to see if the container already contains the product, somewhere in it's tree.
		ProductContainer temp = container;
		ProductContainer next = container.GetParentContainer();
		
		while (next != null)
		{
			temp = next;
			next = next.GetParentContainer();
		}
		
		if (FindProductContainer(temp, product) != null)
			return false;
		
		return true;
	}
	
	/**
	 * Checks to see if the requested edits for a given {@link Product} are valid.
	 * @param original The reference to the original product.
	 * @param edited The product containing the requested changes
	 * @return True if it can be edited. False otherwise.
	 */
	public boolean CanEditProduct(Product original, Product edited) {
		
		if (!Product.IsValid(original) || !Product.IsValid(edited)) {
			return false;
		}
		
		// If the values are different, check if the ProductList contains the new Barcode.
		if (!original.GetBarcode().GetValue().equals(edited.GetBarcode().GetValue()) &&
				allProducts.get(edited.GetBarcode()) != null) {
			return false;
		}
		
		return true;
	}
	
	/**
	 * Checks if a {@link product} can be removed from the system.
	 * @param product The product to be removed
	 * @return True if it can be removed, false otherwise.
	 */
	public boolean CanRemoveProduct(Product product) {
		
		if (!Product.IsValid(product)) {
			return false;
		}
				
		if (!allProducts.containsKey(product.GetBarcode())) {
			return false;
		}
		
		ItemIndex itemIndex = HomeInventoryTracker.Instance().GetItemIndex();
		
		Iterator<Item> iter = itemIndex.GetItemsByProduct(product);
		
		return (!iter.hasNext());
	}
	
	/**
	 * Checks if a {@link Product} can be removed from a given {@link ProductContainer} 
	 * @param product The product to be removed
	 * @param container The container the product should be removed from.
	 * @return True if the product can be removed from the container. False otherwise.
	 */
	public boolean CanRemoveProductFromContainer(Product product, ProductContainer container) {
		
		if (!Product.IsValid(product) || !productsByContainer.get(container).contains(product))
			return false;
		
		ItemIndex itemIndex = HomeInventoryTracker.Instance().GetItemIndex();
		
		Iterator<Item> iter = itemIndex.GetItemsByProduct(container, product);
		
		return (!iter.hasNext());
	}
	
	
	/**
	 * Adds a new {@link Product} to the system.
	 * @param product The product to be added.
	 * @throws IllegalArgumentException if the product can not be added.
	 */
	public void AddProduct(Product product) throws IllegalArgumentException
	{
		if (!CanAddProduct(product))
			throw new IllegalArgumentException();
		
		allProducts.put(product.GetBarcode(), product);
		
		// Don't add any containers at this point.
	}
	
	/**
	 * Adds the {@link Product} to the system.
	 * @param product The product to be added.
	 * @param container The initial {@link ProductContainer} to place the product in.
	 * @throws IllegalArgumentException if the Product is invalid.
	 */
	public void AddProduct(Product product, ProductContainer container)
			throws IllegalArgumentException {
		
		if (!CanAddProduct(product))
			throw new IllegalArgumentException();
		
		allProducts.put(product.GetBarcode(), product);
		
		if (!productsByContainer.containsKey(container))
		{
			Set<Product> newProdList = new HashSet<Product>();
			productsByContainer.put(container, newProdList);
		}
		
		productsByContainer.get(container).add(product);
		product.AddProductContainer(container); // TODO: Do we need this?
	}
	
	/**
	 * Adds a {@link Product} to a given {@link ProductContainer} 
	 * @param product The product to be added.
	 * @param container The container to add the product to.
	 * @throws IllegalArgumentException if the product is not valid
	 */
	public void AddProductToContainer(Product product, ProductContainer container)
			throws IllegalArgumentException {
		
		if (!CanAddProductToContainer(product, container))
			throw new IllegalArgumentException();
		
		// If the container does not exist, return.
		if (!productsByContainer.containsKey(container)) {
			productsByContainer.put(container, new HashSet<Product>());
		}
		
		// If the product is not in the system. Add it as new.
		if (!allProducts.containsKey(product.GetBarcode()))
		{
			AddProduct(product, container);
			return;
		}
		
		productsByContainer.get(container).add(product);
		product.AddProductContainer(container);
	}
	

	/**
	 * Updates a product using information stored in an existing one
	 * @param original The original product that is being updated.
	 * @param edit The product to get the new information from.
	 * @throws IllegalArgumentException if either original or edit is not valid, 
	 * 		or the original is not in the list.
	 */
	public void EditProduct(Product original, Product edit) throws IllegalArgumentException{
		
		if (!Product.IsValid(original) || !Product.IsValid(edit))
		{
			throw new IllegalArgumentException();
		}
		
		Barcode originalKeyValue = original.GetBarcode();
		Barcode editKeyValue = edit.GetBarcode();

		if (allProducts.get(originalKeyValue) == null 
				|| (!originalKeyValue.equals(editKeyValue) && 
						allProducts.get(editKeyValue) != null)) {
			
			throw new IllegalArgumentException();
		}
		
		allProducts.remove(original.GetBarcode());
		
		original.SetBarcode(edit.GetBarcode());
		original.SetCreation(edit.GetCreation());
		original.SetDescription(edit.GetDescription());
		original.SetShelfLife(edit.GetShelfLife());
		original.SetSize(edit.GetSize());
		original.SetThreeMonth(edit.GetThreeMonth());
		
		allProducts.put(original.GetBarcode(), original);
	}
	
	
	/**
	 * Removes a {@link Product} from a {@link ProductContainer} and updates the index
	 * @param product the Product to be removed.
	 * @param container the ProductContainer to add the Product to.
	 * @throws IllegalArgumentException if the Product is not valid or not in ProductContainer
	 * */
	public void RemoveProductFromContainer(Product product, ProductContainer container) {
		if (!CanRemoveProductFromContainer(product, container))
			throw new IllegalArgumentException("Product cannot be removed from ProductContainer");
		
		product.RemoveProductContainer(container);
		productsByContainer.get(container).remove(product);
	}

	
	/**
	 * Removes a product from the index.
	 * @param product The product to be removed.
	 */
	public void RemoveProduct(Product product) throws IllegalArgumentException {
		if (!CanRemoveProduct(product))
			throw new IllegalArgumentException();
		
		// Delete all removed items.
		ItemIndex itemIndex = HomeInventoryTracker.Instance().GetItemIndex();
		itemIndex.DeleteFromRemovedItems(product);		

		// Remove from ProductContainers
		Iterator<ProductContainer> iter = product.GetProductContainerIterator();
		
		while (iter.hasNext())
		{
			ProductContainer container = iter.next();
			
			if (productsByContainer.get(container).contains(product))
			{
				productsByContainer.get(container).remove(product);
			}
		}
		
		// Remove from ProductList.
		allProducts.remove(product.GetBarcode());
	}

	
	/**
	 * Transfers a {@link product} to a given {@link ProductContainer}. If the product is not
	 * 	already within that {@link StorageUnit}, then the product is simply added to the
	 *  destination.
	 * @param product The product to be transfered.
	 * @param destination The ProductContainer the product should be transfered to
	 * @throws IllegalArgumentException If either the product or the destination are invalid.
	 */
	public void TransferProduct(Product product, ProductContainer destination)
		throws IllegalArgumentException {
		
		if (!Product.IsValid(product) || destination == null ||
				!destination.IsValid())
		{
			throw new IllegalArgumentException();
		}
		
		StorageUnit root = destination.GetRootContainer();
		if (product.IsInStorageUnit(root))
		{
			ProductContainer initial = null;
			
			Iterator<ProductContainer> iter = product.GetProductContainerIterator();
			
			while (iter.hasNext()) {
				ProductContainer temp = iter.next();
			
				if (temp.GetRootContainer() == root) {
					initial = temp;
					break;
				}
			}
			
			if (initial == destination) {
				return;
			}
			
			ItemIndex itemIndex = HomeInventoryTracker.Instance().GetItemIndex();
			itemIndex.TransferAllItems(product, initial, destination);
			
			RemoveProductFromContainer(product, initial);
			AddProductToContainer(product, destination);
		}
		else
		{
			AddProductToContainer(product, destination);
		}
	}
	
	public void Accept(Visitor visitor){
		visitor.VisitProductIndex(this);
		Set<Barcode> keyset = allProducts.keySet();
		for (Barcode barcode : keyset){
			allProducts.get(barcode).Accept(visitor);
		}
	}
}
